/**
 * The contents of this file are subject to the license and copyright
 * detailed in the LICENSE and NOTICE files at the root of the source
 * tree and available online at
 *
 * http://www.dspace.org/license/
 */
package org.dspace.browse;

import java.io.IOException;
import java.util.ArrayList;
import java.util.StringTokenizer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.dspace.services.ConfigurationService;
import org.dspace.services.factory.DSpaceServicesFactory;
import org.dspace.sort.SortException;
import org.dspace.sort.SortOption;

/**
 * This class holds all the information about a specifically configured
 * BrowseIndex.  It is responsible for parsing the configuration, understanding
 * about what sort options are available, and what the names of the database
 * tables that hold all the information are actually called. Hierarchical browse
 * indexes also contain information about the vocabulary they're using, see:
 * {@link org.dspace.content.authority.DSpaceControlledVocabularyIndex}
 *
 * @author Richard Jones
 */
public class BrowseIndex {
    /** the configuration number, as specified in the config */
    /**
     * used for single metadata browse tables for generating the table name
     */
    private int number;

    /**
     * the name of the browse index, as specified in the config
     */
    private String name;

    /**
     * the SortOption for this index (only valid for item indexes)
     */
    private SortOption sortOption;

    /**
     * the value of the metadata, as specified in the config
     */
    private String metadataAll;

    /**
     * the metadata fields, as an array
     */
    private String[] metadata;

    /**
     * the datatype of the index, as specified in the config
     */
    private String datatype;

    /**
     * the display type of the metadata, as specified in the config
     */
    private String displayType;

    /**
     * base name for tables, sequences
     */
    private String tableBaseName;

    /**
     * a three part array of the metadata bits (e.g. dc.contributor.author)
     */
    private String[][] mdBits;

    /**
     * default order (asc / desc) for this index
     */
    private String defaultOrder = SortOption.ASCENDING;

    /**
     * whether to display frequencies or not, in case of a "metadata" browse index
     */
    private boolean displayFrequencies = true;

    /**
     * additional 'internal' tables that are always defined
     */
    private static BrowseIndex itemIndex = new BrowseIndex("bi_item");
    private static BrowseIndex withdrawnIndex = new BrowseIndex("bi_withdrawn");
    private static BrowseIndex privateIndex = new BrowseIndex("bi_private");


    /**
     * Ensure no one else can create these
     */
    private BrowseIndex() {
    }

    /**
     * Constructor for creating generic / internal index objects
     *
     * @param baseName The base of the table name
     */
    protected BrowseIndex(String baseName) {
        try {
            number = -1;
            tableBaseName = baseName;
            displayType = "item";
            sortOption = SortOption.getDefaultSortOption();
        } catch (SortException se) {
            // FIXME Exception handling
        }
    }

    /**
     * Create a new BrowseIndex object using the definition from the configuration,
     * and the number of the configuration option.  The definition should follow
     * one of the following forms:
     *
     * <code>
     * [name]:item:[sort option]:[order]
     * </code>
     *
     * or
     *
     * <code>
     * [name]:metadata:[metadata]:[data type]:[order]:[sort option]
     * </code>
     *
     * [name] is a freetext name for the field
     * item or metadata defines the display type
     * [metadata] is the usual format of the metadata such as dc.contributor.author
     * [sort option] is the name of a separately defined sort option
     * [order] must be either asc or desc
     * [data type] must be either "title", "date" or "text"
     *
     * If you use the first form (to define an index of type item), the order
     * is facultative. If you use the second form (for type metadata), the order
     * and sort option are facultative, but you must configure the order if you
     * want to configure the sort option.
     *
     * @param definition the configuration definition of this index
     * @param number     the configuration number of this index
     * @throws BrowseException if browse error
     */
    private BrowseIndex(String definition, int number)
        throws BrowseException {
        try {
            boolean valid = true;
            this.defaultOrder = SortOption.ASCENDING;
            this.number = number;

            String rx = "(\\w+):(\\w+):([\\w\\.\\*,]+):?(\\w*):?(\\w*):?(\\w*)";
            Pattern pattern = Pattern.compile(rx);
            Matcher matcher = pattern.matcher(definition);

            if (matcher.matches()) {
                name = matcher.group(1);
                displayType = matcher.group(2);

                if (isMetadataIndex()) {
                    metadataAll = matcher.group(3);
                    datatype = matcher.group(4);

                    if (metadataAll != null) {
                        metadata = metadataAll.split(",");
                    }

                    if (metadata == null || metadata.length == 0) {
                        valid = false;
                    }

                    if (datatype == null || datatype.equals("")) {
                        valid = false;
                    }

                    // If an optional ordering configuration is supplied,
                    // set the defaultOrder appropriately (asc or desc)
                    if (matcher.groupCount() > 4) {
                        String order = matcher.group(5);
                        if (SortOption.DESCENDING.equalsIgnoreCase(order)) {
                            this.defaultOrder = SortOption.DESCENDING;
                        }
                    }

                    if (matcher.groupCount() > 5) {
                        String sortName = matcher.group(6).trim();
                        if (sortName.length() > 0) {
                            for (SortOption so : SortOption.getSortOptions()) {
                                if (so.getName().equals(sortName)) {
                                    sortOption = so;
                                }
                            }

                            // for backward compatability we ignore the keywords
                            // single and full here
                            if (!sortName.equalsIgnoreCase("single")
                                && !sortName.equalsIgnoreCase("full")
                                && sortOption == null) {
                                valid = false;
                            }
                        }
                    }

                    tableBaseName = getItemBrowseIndex().tableBaseName;
                } else if (isItemIndex()) {
                    String sortName = matcher.group(3);

                    for (SortOption so : SortOption.getSortOptions()) {
                        if (so.getName().equals(sortName)) {
                            sortOption = so;
                        }
                    }

                    if (sortOption == null) {
                        valid = false;
                    }

                    // If an optional ordering configuration is supplied,
                    // set the defaultOrder appropriately (asc or desc)
                    if (matcher.groupCount() > 3) {
                        String order = matcher.group(4);
                        if (SortOption.DESCENDING.equalsIgnoreCase(order)) {
                            this.defaultOrder = SortOption.DESCENDING;
                        }
                    }

                    tableBaseName = getItemBrowseIndex().tableBaseName;
                } else {
                    valid = false;
                }
            } else {
                valid = false;
            }

            if (!valid) {
                throw new BrowseException("Browse Index configuration is not valid: webui.browse.index." +
                                              number + " = " + definition);
            }
        } catch (SortException se) {
            throw new BrowseException("Error in SortOptions", se);
        }
    }

    /**
     * @return Default order for this index, null if not specified
     */
    public String getDefaultOrder() {
        return defaultOrder;
    }

    /**
     * @return Returns the datatype.
     */
    public String getDataType() {
        if (sortOption != null) {
            return sortOption.getType();
        }

        return datatype;
    }

    /**
     * @return Returns the displayType.
     */
    public String getDisplayType() {
        return displayType;
    }

    /**
     * @return Returns the number of metadata fields for this index
     */
    public int getMetadataCount() {
        if (isMetadataIndex()) {
            return metadata.length;
        }

        return 0;
    }

    /**
     * @param idx index
     * @return Returns the mdBits.
     */
    public String[] getMdBits(int idx) {
        if (isMetadataIndex()) {
            return mdBits[idx];
        }

        return null;
    }

    /**
     * @return Returns the metadata.
     */
    public String getMetadata() {
        return metadataAll;
    }

    /**
     * @param idx index
     * @return metadata
     */
    public String getMetadata(int idx) {
        return metadata[idx];
    }

    /**
     * @return Returns the name.
     */
    public String getName() {
        return name;
    }

    /**
     * Get the SortOption associated with this index.
     *
     * @return SortOption
     */
    public SortOption getSortOption() {
        return sortOption;
    }

    /**
     * @return true or false
     */
    public boolean isDisplayFrequencies() {
        return displayFrequencies;
    }

    /**
     * Populate the internal array containing the bits of metadata, for
     * ease of use later
     */
    public void generateMdBits() {
        try {
            if (isMetadataIndex()) {
                mdBits = new String[metadata.length][];
                for (int i = 0; i < metadata.length; i++) {
                    mdBits[i] = interpretField(metadata[i], null);
                }
            }
        } catch (IOException e) {
            // it's not obvious what we really ought to do here
            //log.error("caught exception: ", e);
        }
    }

    /**
     * Get the name of the sequence that will be used in the given circumstances
     *
     * @param isDistinct is a distinct table
     * @param isMap      is a map table
     * @return the name of the sequence
     */
    public String getSequenceName(boolean isDistinct, boolean isMap) {
        if (isDistinct || isMap) {
            return BrowseIndex.getSequenceName(number, isDistinct, isMap);
        }

        return BrowseIndex.getSequenceName(tableBaseName, isDistinct, isMap);
    }

    /**
     * Get the name of the sequence that will be used in the given circumstances
     *
     * @param number     the index configuration number
     * @param isDistinct is a distinct table
     * @param isMap      is a map table
     * @return the name of the sequence
     */
    public static String getSequenceName(int number, boolean isDistinct, boolean isMap) {
        return BrowseIndex.getSequenceName(makeTableBaseName(number), isDistinct, isMap);
    }

    /**
     * Generate a sequence name from the given base
     *
     * @param baseName
     * @param isDistinct
     * @param isMap
     * @return
     */
    private static String getSequenceName(String baseName, boolean isDistinct, boolean isMap) {
        if (isDistinct) {
            baseName = baseName + "_dis";
        } else if (isMap) {
            baseName = baseName + "_dmap";
        }

        baseName = baseName + "_seq";

        return baseName;
    }

    /**
     * Get the name of the table for the given set of circumstances
     * This is provided solely for cleaning the database, where you are
     * trying to create table names that may not be reflected in the current index
     *
     * @param number       the index configuration number
     * @param isCommunity  whether this is a community constrained index (view)
     * @param isCollection whether this is a collection constrained index (view)
     * @param isDistinct   whether this is a distinct table
     * @param isMap        whether this is a distinct map table
     * @return the name of the table
     * @deprecated 1.5
     */
    @Deprecated
    public static String getTableName(int number, boolean isCommunity, boolean isCollection, boolean isDistinct,
                                      boolean isMap) {
        return BrowseIndex.getTableName(makeTableBaseName(number), isCommunity, isCollection, isDistinct, isMap);
    }

    /**
     * Generate a table name from the given base
     *
     * @param baseName     base name
     * @param isCommunity  whether this is a community constrained index (view)
     * @param isCollection whether this is a collection constrained index (view)
     * @param isDistinct   whether this is a distinct table
     * @param isMap        whether this is a distinct map table
     * @return table name
     */
    private static String getTableName(String baseName, boolean isCommunity, boolean isCollection, boolean isDistinct,
                                       boolean isMap) {
        // isDistinct is meaningless in relation to isCommunity and isCollection
        // so we bounce that back first, ignoring other arguments
        if (isDistinct) {
            return baseName + "_dis";
        }

        // isCommunity and isCollection are mutually exclusive
        if (isCommunity) {
            baseName = baseName + "_com";
        } else if (isCollection) {
            baseName = baseName + "_col";
        }

        // isMap is additive to isCommunity and isCollection
        if (isMap) {
            baseName = baseName + "_dmap";
        }

        return baseName;
    }

    /**
     * Get the name of the table in the given circumstances
     *
     * @param isCommunity  whether this is a community constrained index (view)
     * @param isCollection whether this is a collection constrained index (view)
     * @param isDistinct   whether this is a distinct table
     * @param isMap        whether this is a distinct map table
     * @return the name of the table
     * @deprecated 1.5
     */
    @Deprecated
    public String getTableName(boolean isCommunity, boolean isCollection, boolean isDistinct, boolean isMap) {
        if (isDistinct || isMap) {
            return BrowseIndex.getTableName(number, isCommunity, isCollection, isDistinct, isMap);
        }

        return BrowseIndex.getTableName(tableBaseName, isCommunity, isCollection, isDistinct, isMap);
    }

    /**
     * Get the name of the table in the given circumstances.  This is the same as calling
     *
     * <code>
     * getTableName(isCommunity, isCollection, false, false);
     * </code>
     *
     * @param isCommunity  whether this is a community constrained index (view)
     * @param isCollection whether this is a collection constrained index (view)
     * @return the name of the table
     * @deprecated 1.5
     */
    @Deprecated
    public String getTableName(boolean isCommunity, boolean isCollection) {
        return getTableName(isCommunity, isCollection, false, false);
    }

    /**
     * Get the default index table name.  This is the same as calling
     *
     * <code>
     * getTableName(false, false, false, false);
     * </code>
     *
     * @return table name
     */
    public String getTableName() {
        return getTableName(false, false, false, false);
    }

    /**
     * Get the table name for the given set of circumstances.
     *
     * This is the same as calling:
     *
     * <code>
     * getTableName(isCommunity, isCollection, isDistinct, false);
     * </code>
     *
     * @param isCommunity  whether this is a community constrained index (view)
     * @param isCollection whether this is a collection constrained index (view)
     * @param isDistinct   whether this is a distinct table
     * @return table name
     * @deprecated 1.5
     */
    @Deprecated
    public String getTableName(boolean isDistinct, boolean isCommunity, boolean isCollection) {
        return getTableName(isCommunity, isCollection, isDistinct, false);
    }

    /**
     * Get the default name of the distinct map table.  This is the same as calling
     *
     * <code>
     * getTableName(false, false, false, true);
     * </code>
     *
     * @return table name
     */
    public String getMapTableName() {
        return getTableName(false, false, false, true);
    }

    /**
     * Get the default name of the distinct table.  This is the same as calling
     *
     * <code>
     * getTableName(false, false, true, false);
     * </code>
     *
     * @return table name
     */
    public String getDistinctTableName() {
        return getTableName(false, false, true, false);
    }

    /**
     * Get the name of the column that is used to store the default value column
     *
     * @return the name of the value column
     */
    public String getValueColumn() {
        if (!isDate()) {
            return "sort_text_value";
        } else {
            return "text_value";
        }
    }

    /**
     * Get the name of the primary key index column
     *
     * @return the name of the primary key index column
     */
    public String getIndexColumn() {
        return "id";
    }

    /**
     * Is this browse index type for a title?
     *
     * @return true if title type, false if not
     */
//    public boolean isTitle()
//    {
//        return "title".equals(getDataType());
//    }

    /**
     * Is the browse index type for a date?
     *
     * @return true if date type, false if not
     */
    public boolean isDate() {
        return "date".equals(getDataType());
    }

    /**
     * Is the browse index type for a plain text type?
     *
     * @return true if plain text type, false if not
     */
//    public boolean isText()
//    {
//        return "text".equals(getDataType());
//    }

    /**
     * Is the browse index of display type single?
     *
     * @return true if singe, false if not
     */
    public boolean isMetadataIndex() {
        return displayType != null && displayType.startsWith("metadata");
    }

    /**
     * Is the browse index authority value?
     *
     * @return true if authority, false if not
     */
    public boolean isAuthorityIndex() {
        return "metadataAuthority".equals(displayType);
    }

    /**
     * Is the browse index of display type full?
     *
     * @return true if full, false if not
     */
    public boolean isItemIndex() {
        return "item".equals(displayType);
    }

    /**
     * Get the field for sorting associated with this index.
     *
     * @param isSecondLevel whether second level browse
     * @return sort field
     * @throws BrowseException if browse error
     */
    public String getSortField(boolean isSecondLevel) throws BrowseException {
        String focusField;
        if (isMetadataIndex() && !isSecondLevel) {
            focusField = "sort_value";
        } else {
            if (sortOption != null) {
                focusField = "sort_" + sortOption.getNumber();
            } else {
                focusField = "sort_1";  // Use the first sort column
            }
        }

        return focusField;
    }

    /**
     * @return array of tables
     * @throws BrowseException if browse error
     * @deprecated
     */
    @Deprecated
    public static String[] tables()
        throws BrowseException {
        BrowseIndex[] bis = getBrowseIndices();
        String[] returnTables = new String[bis.length];
        for (int i = 0; i < bis.length; i++) {
            returnTables[i] = bis[i].getTableName();
        }

        return returnTables;
    }

    /**
     * Get an array of all the browse indices for the current configuration
     *
     * @return an array of all the current browse indices
     * @throws BrowseException if browse error
     */
    public static BrowseIndex[] getBrowseIndices()
        throws BrowseException {
        int idx = 1;
        String definition;
        ArrayList<BrowseIndex> browseIndices = new ArrayList<>();

        ConfigurationService configurationService
                = DSpaceServicesFactory.getInstance().getConfigurationService();
        while (((definition = configurationService.getProperty("webui.browse.index." + idx))) != null) {
            BrowseIndex bi = new BrowseIndex(definition, idx);
            bi.displayFrequencies = configurationService
                    .getBooleanProperty("webui.browse.metadata.show-freq." + idx, true);

            browseIndices.add(bi);
            idx++;
        }

        BrowseIndex[] bis = new BrowseIndex[browseIndices.size()];
        bis = browseIndices.toArray(bis);

        return bis;
    }

    /**
     * Get the browse index from configuration with the specified name.
     * The name is the first part of the browse configuration
     *
     * @param name the name to retrieve
     * @return the specified browse index
     * @throws BrowseException if browse error
     */
    public static BrowseIndex getBrowseIndex(String name)
        throws BrowseException {
        for (BrowseIndex bix : BrowseIndex.getBrowseIndices()) {
            if (bix.getName().equals(name)) {
                return bix;
            }
        }

        return null;
    }

    /**
     * Get the configured browse index that is defined to use this sort option.
     *
     * @param so sort option
     * @return browse index
     * @throws BrowseException if browse error
     */
    public static BrowseIndex getBrowseIndex(SortOption so) throws BrowseException {
        for (BrowseIndex bix : BrowseIndex.getBrowseIndices()) {
            if (bix.getSortOption() == so) {
                return bix;
            }
        }

        return null;
    }

    /**
     * Get the internally defined browse index for archived items.
     *
     * @return browse index
     */
    public static BrowseIndex getItemBrowseIndex() {
        return BrowseIndex.itemIndex;
    }

    /**
     * Get the internally defined browse index for withdrawn items.
     *
     * @return browse index
     */
    public static BrowseIndex getWithdrawnBrowseIndex() {
        return BrowseIndex.withdrawnIndex;
    }

    /**
     * @return browse index
     */
    public static BrowseIndex getPrivateBrowseIndex() {
        return BrowseIndex.privateIndex;
    }

    /**
     * Take a string representation of a metadata field, and return it as an array.
     * This is just a convenient utility method to basically break the metadata
     * representation up by its delimiter (.), and stick it in an array, inserting
     * the value of the init parameter when there is no metadata field part.
     *
     * @param mfield the string representation of the metadata
     * @param init   the default value of the array elements
     * @return a three element array with schema, element and qualifier respectively
     * @throws IOException if IO error
     */
    public String[] interpretField(String mfield, String init)
        throws IOException {
        StringTokenizer sta = new StringTokenizer(mfield, ".");
        String[] field = {init, init, init};

        int i = 0;
        while (sta.hasMoreTokens()) {
            field[i++] = sta.nextToken();
        }

        // error checks to make sure we have at least a schema and qualifier for both
        if (field[0] == null || field[1] == null) {
            throw new IOException("at least a schema and element be " +
                                      "specified in configuration.  You supplied: " + mfield);
        }

        return field;
    }

    /**
     * Does this browse index represent one of the internal item indexes?
     *
     * @return true or false
     */
    public boolean isInternalIndex() {
        return (this == itemIndex || this == withdrawnIndex || this == privateIndex);
    }

    /**
     * Generate a base table name.
     *
     * @param number index number
     * @return table name
     */
    private static String makeTableBaseName(int number) {
        return "bi_" + Integer.toString(number);
    }

    /**
     * Is tag cloud enabled
     *
     * @return true or false
     */
    public boolean isTagCloudEnabled() {
        ConfigurationService configurationService
                = DSpaceServicesFactory.getInstance().getConfigurationService();
        return configurationService.getBooleanProperty("webui.browse.index.tagcloud." + number);
    }
}
